using System;
using SunflowSharp.Core;
using SunflowSharp.Maths;

namespace SunflowSharp.Core.Primitive
{

    public abstract class CubeGrid : PrimitiveList
    {
        private int nx, ny, nz;
        private float voxelwx, voxelwy, voxelwz;
        private float invVoxelwx, invVoxelwy, invVoxelwz;
        private BoundingBox bounds;

        public CubeGrid()
        {
            nx = ny = nz = 1;
            bounds = new BoundingBox(1);
        }

        public bool update(ParameterList pl, SunflowAPI api)
        {
            nx = pl.getInt("resolutionX", nx);
            ny = pl.getInt("resolutionY", ny);
            nz = pl.getInt("resolutionZ", nz);
            voxelwx = 2.0f / nx;
            voxelwy = 2.0f / ny;
            voxelwz = 2.0f / nz;
            invVoxelwx = 1 / voxelwx;
            invVoxelwy = 1 / voxelwy;
            invVoxelwz = 1 / voxelwz;
            return true;
        }

        protected abstract bool inside(int x, int y, int z);

        public BoundingBox getBounds()
        {
            return bounds;
        }

        public void prepareShadingState(ShadingState state)
        {
            state.init();
            state.getRay().getPoint(state.getPoint());
            Instance parent = state.getInstance();
            Vector3 normal;
            switch (state.getPrimitiveID())
            {
                case 0:
                    normal = new Vector3(-1, 0, 0);
                    break;
                case 1:
                    normal = new Vector3(1, 0, 0);
                    break;
                case 2:
                    normal = new Vector3(0, -1, 0);
                    break;
                case 3:
                    normal = new Vector3(0, 1, 0);
                    break;
                case 4:
                    normal = new Vector3(0, 0, -1);
                    break;
                case 5:
                    normal = new Vector3(0, 0, 1);
                    break;
                default:
                    normal = new Vector3(0, 0, 0);
                    break;
            }
            state.getNormal().set(parent.transformNormalObjectToWorld(normal));
            state.getGeoNormal().set(state.getNormal());
            state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal()));
            state.setShader(parent.getShader(0));
            state.setModifier(parent.getModifier(0));
        }

        public void intersectPrimitive(Ray r, int primID, IntersectionState state)
        {
            float intervalMin = r.getMin();
            float intervalMax = r.getMax();
            float orgX = r.ox;
            float orgY = r.oy;
            float orgZ = r.oz;
            float dirX = r.dx, invDirX = 1 / dirX;
            float dirY = r.dy, invDirY = 1 / dirY;
            float dirZ = r.dz, invDirZ = 1 / dirZ;
            float t1, t2;
            t1 = (-1 - orgX) * invDirX;
            t2 = (+1 - orgX) * invDirX;
            int curr = -1;
            if (invDirX > 0)
            {
                if (t1 > intervalMin)
                {
                    intervalMin = t1;
                    curr = 0;
                }
                if (t2 < intervalMax)
                    intervalMax = t2;
                if (intervalMin > intervalMax)
                    return;
            }
            else
            {
                if (t2 > intervalMin)
                {
                    intervalMin = t2;
                    curr = 1;
                }
                if (t1 < intervalMax)
                    intervalMax = t1;
                if (intervalMin > intervalMax)
                    return;
            }
            t1 = (-1 - orgY) * invDirY;
            t2 = (+1 - orgY) * invDirY;
            if (invDirY > 0)
            {
                if (t1 > intervalMin)
                {
                    intervalMin = t1;
                    curr = 2;
                }
                if (t2 < intervalMax)
                    intervalMax = t2;
                if (intervalMin > intervalMax)
                    return;
            }
            else
            {
                if (t2 > intervalMin)
                {
                    intervalMin = t2;
                    curr = 3;
                }
                if (t1 < intervalMax)
                    intervalMax = t1;
                if (intervalMin > intervalMax)
                    return;
            }
            t1 = (-1 - orgZ) * invDirZ;
            t2 = (+1 - orgZ) * invDirZ;
            if (invDirZ > 0)
            {
                if (t1 > intervalMin)
                {
                    intervalMin = t1;
                    curr = 4;
                }
                if (t2 < intervalMax)
                    intervalMax = t2;
                if (intervalMin > intervalMax)
                    return;
            }
            else
            {
                if (t2 > intervalMin)
                {
                    intervalMin = t2;
                    curr = 5;
                }
                if (t1 < intervalMax)
                    intervalMax = t1;
                if (intervalMin > intervalMax)
                    return;
            }
            // box is hit at [intervalMin, intervalMax]
            orgX += intervalMin * dirX;
            orgY += intervalMin * dirY;
            orgZ += intervalMin * dirZ;
            // locate starting point inside the grid
            // and set up 3D-DDA vars
            int indxX, indxY, indxZ;
            int stepX, stepY, stepZ;
            int stopX, stopY, stopZ;
            float deltaX, deltaY, deltaZ;
            float tnextX, tnextY, tnextZ;
            // stepping factors along X
            indxX = (int)((orgX + 1) * invVoxelwx);
            if (indxX < 0)
                indxX = 0;
            else if (indxX >= nx)
                indxX = nx - 1;
            if (Math.Abs(dirX) < 1e-6f)
            {
                stepX = 0;
                stopX = indxX;
                deltaX = 0;
                tnextX = float.PositiveInfinity;
            }
            else if (dirX > 0)
            {
                stepX = 1;
                stopX = nx;
                deltaX = voxelwx * invDirX;
                tnextX = intervalMin + ((indxX + 1) * voxelwx - 1 - orgX) * invDirX;
            }
            else
            {
                stepX = -1;
                stopX = -1;
                deltaX = -voxelwx * invDirX;
                tnextX = intervalMin + (indxX * voxelwx - 1 - orgX) * invDirX;
            }
            // stepping factors along Y
            indxY = (int)((orgY + 1) * invVoxelwy);
            if (indxY < 0)
                indxY = 0;
            else if (indxY >= ny)
                indxY = ny - 1;
            if (Math.Abs(dirY) < 1e-6f)
            {
                stepY = 0;
                stopY = indxY;
                deltaY = 0;
                tnextY = float.PositiveInfinity;
            }
            else if (dirY > 0)
            {
                stepY = 1;
                stopY = ny;
                deltaY = voxelwy * invDirY;
                tnextY = intervalMin + ((indxY + 1) * voxelwy - 1 - orgY) * invDirY;
            }
            else
            {
                stepY = -1;
                stopY = -1;
                deltaY = -voxelwy * invDirY;
                tnextY = intervalMin + (indxY * voxelwy - 1 - orgY) * invDirY;
            }
            // stepping factors along Z
            indxZ = (int)((orgZ + 1) * invVoxelwz);
            if (indxZ < 0)
                indxZ = 0;
            else if (indxZ >= nz)
                indxZ = nz - 1;
            if (Math.Abs(dirZ) < 1e-6f)
            {
                stepZ = 0;
                stopZ = indxZ;
                deltaZ = 0;
                tnextZ = float.PositiveInfinity;
            }
            else if (dirZ > 0)
            {
                stepZ = 1;
                stopZ = nz;
                deltaZ = voxelwz * invDirZ;
                tnextZ = intervalMin + ((indxZ + 1) * voxelwz - 1 - orgZ) * invDirZ;
            }
            else
            {
                stepZ = -1;
                stopZ = -1;
                deltaZ = -voxelwz * invDirZ;
                tnextZ = intervalMin + (indxZ * voxelwz - 1 - orgZ) * invDirZ;
            }
            // are we starting inside the cube
            bool isInside = inside(indxX, indxY, indxZ) && bounds.contains(r.ox, r.oy, r.oz);
            // trace through the grid
            for (; ; )
            {
                if (inside(indxX, indxY, indxZ) != isInside)
                {
                    // we hit a boundary
                    r.setMax(intervalMin);
                    // if we are inside, the last bit needs to be flipped
                    if (isInside)
                        curr ^= 1;
                    state.setIntersection(curr, 0, 0);
                    return;
                }
                if (tnextX < tnextY && tnextX < tnextZ)
                {
                    curr = dirX > 0 ? 0 : 1;
                    intervalMin = tnextX;
                    if (intervalMin > intervalMax)
                        return;
                    indxX += stepX;
                    if (indxX == stopX)
                        return;
                    tnextX += deltaX;
                }
                else if (tnextY < tnextZ)
                {
                    curr = dirY > 0 ? 2 : 3;
                    intervalMin = tnextY;
                    if (intervalMin > intervalMax)
                        return;
                    indxY += stepY;
                    if (indxY == stopY)
                        return;
                    tnextY += deltaY;
                }
                else
                {
                    curr = dirZ > 0 ? 4 : 5;
                    intervalMin = tnextZ;
                    if (intervalMin > intervalMax)
                        return;
                    indxZ += stepZ;
                    if (indxZ == stopZ)
                        return;
                    tnextZ += deltaZ;
                }
            }
        }

        public int getNumPrimitives()
        {
            return 1;
        }

        public float getPrimitiveBound(int primID, int i)
        {
            return ((i & 1) == 0) ? -1 : 1;
        }

        public BoundingBox getWorldBounds(Matrix4 o2w)
        {
            if (o2w == null)
                return bounds;
            return o2w.transform(bounds);
        }

        public abstract PrimitiveList getBakingPrimitives();
    }
}